package jrain.fw.script.tools;

import java.beans.BeanInfo;
import java.beans.EventSetDescriptor;
import java.beans.IndexedPropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import jrain.fw.script.operator.Constants;
 
public class BeanInfoManager {
	// -------------------------------------
	// Properties
	// -------------------------------------
	// property beanClass

	Class<?> mBeanClass;

	public Class<?> getBeanClass() {
		return mBeanClass;
	}

	// -------------------------------------
	// Member variables
	// -------------------------------------

	// The BeanInfo
	BeanInfo mBeanInfo;

	// Mapping from property name to BeanInfoProperty
	Map<Object, Object> mPropertyByName;

	// Mapping from property name to BeanInfoIndexedProperty
	Map<Object, Object> mIndexedPropertyByName;

	// Mapping from event set name to event set descriptor
	Map<Object, Object> mEventSetByName;

	// Flag if this is initialized
	boolean mInitialized;

	// The global mapping from class to BeanInfoManager
	static Map<Object, Object> mBeanInfoManagerByClass = new HashMap<Object, Object>();

	// -------------------------------------

	/**
	 * Constructor
	 */
	BeanInfoManager(Class<?> pBeanClass) {
		mBeanClass = pBeanClass;
	}

	// -------------------------------------

	/**
	 * Returns the BeanInfoManager for the specified class
	 */
	public static BeanInfoManager getBeanInfoManager(Class<?> pClass) {
		BeanInfoManager ret = (BeanInfoManager) mBeanInfoManagerByClass.get(pClass);
		if (ret == null) {
			ret = createBeanInfoManager(pClass);
		}
		return ret;
	}

	// -------------------------------------

	/**
	 * Creates and registers the BeanInfoManager for the given class if it isn't already registered.
	 */
	static synchronized BeanInfoManager createBeanInfoManager(Class<?> pClass) {
		// Because this method is synchronized statically, the
		// BeanInfoManager is not initialized at this time (otherwise it
		// could end up being a bottleneck for the entire system). It is
		// put into the map in an uninitialized state. The first time
		// someone tries to use it, it will be initialized (with proper
		// synchronizations in place to make sure it is only initialized
		// once).

		BeanInfoManager ret = (BeanInfoManager) mBeanInfoManagerByClass.get(pClass);
		if (ret == null) {
			ret = new BeanInfoManager(pClass);
			mBeanInfoManagerByClass.put(pClass, ret);
		}
		return ret;
	}

	// -------------------------------------

	/**
	 * Returns the BeanInfoProperty for the specified property in the given class, or null if not found.
	 */
	public static BeanInfoProperty getBeanInfoProperty(Class<?> pClass, String pPropertyName, Logger pLogger) throws ELException {
		return getBeanInfoManager(pClass).getProperty(pPropertyName, pLogger);
	}

	// -------------------------------------

	/**
	 * Returns the BeanInfoIndexedProperty for the specified property in the given class, or null if not found.
	 */
	public static BeanInfoIndexedProperty getBeanInfoIndexedProperty(Class<?> pClass, String pIndexedPropertyName, Logger pLogger) throws ELException {
		return getBeanInfoManager(pClass).getIndexedProperty(pIndexedPropertyName, pLogger);
	}

	// -------------------------------------

	/**
	 * Makes sure that this class has been initialized, and synchronizes the initialization if it's required.
	 */
	void checkInitialized(Logger pLogger) throws ELException {
		if (!mInitialized) {
			synchronized (this) {
				if (!mInitialized) {
					initialize(pLogger);
					mInitialized = true;
				}
			}
		}
	}

	// -------------------------------------

	/**
	 * Initializes by mapping property names to BeanInfoProperties
	 */
	void initialize(Logger pLogger) throws ELException {
		try {
			mBeanInfo = Introspector.getBeanInfo(mBeanClass);

			mPropertyByName = new HashMap<Object, Object>();
			mIndexedPropertyByName = new HashMap<Object, Object>();
			PropertyDescriptor[] pds = mBeanInfo.getPropertyDescriptors();
			for (int i = 0; pds != null && i < pds.length; i++) {
				// Treat as both an indexed property and a normal property
				PropertyDescriptor pd = pds[i];
				if (pd instanceof IndexedPropertyDescriptor) {
					IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
					Method readMethod = getPublicMethod(ipd.getIndexedReadMethod());
					Method writeMethod = getPublicMethod(ipd.getIndexedWriteMethod());
					BeanInfoIndexedProperty property = new BeanInfoIndexedProperty(readMethod, writeMethod, ipd);

					mIndexedPropertyByName.put(ipd.getName(), property);
				}

				Method readMethod = getPublicMethod(pd.getReadMethod());
				Method writeMethod = getPublicMethod(pd.getWriteMethod());
				BeanInfoProperty property = new BeanInfoProperty(readMethod, writeMethod, pd);

				mPropertyByName.put(pd.getName(), property);
			}

			mEventSetByName = new HashMap<Object, Object>();
			EventSetDescriptor[] esds = mBeanInfo.getEventSetDescriptors();
			for (int i = 0; esds != null && i < esds.length; i++) {
				EventSetDescriptor esd = esds[i];
				mEventSetByName.put(esd.getName(), esd);
			}
		} catch (IntrospectionException exc) {
			if (pLogger.isLoggingWarning()) {
				pLogger.logWarning(Constants.EXCEPTION_GETTING_BEANINFO, exc, mBeanClass.getName());
			}
		}
	}

	// -------------------------------------

	/**
	 * Returns the BeanInfo for the class
	 */
	BeanInfo getBeanInfo(Logger pLogger) throws ELException {
		checkInitialized(pLogger);
		return mBeanInfo;
	}

	// -------------------------------------

	/**
	 * Returns the BeanInfoProperty for the given property name, or null if not found.
	 */
	public BeanInfoProperty getProperty(String pPropertyName, Logger pLogger) throws ELException {
		checkInitialized(pLogger);
		return (BeanInfoProperty) mPropertyByName.get(pPropertyName);
	}

	// -------------------------------------

	/**
	 * Returns the BeanInfoIndexedProperty for the given property name, or null if not found.
	 */
	public BeanInfoIndexedProperty getIndexedProperty(String pIndexedPropertyName, Logger pLogger) throws ELException {
		checkInitialized(pLogger);
		return (BeanInfoIndexedProperty) mIndexedPropertyByName.get(pIndexedPropertyName);
	}

	// -------------------------------------

	/**
	 * Returns the EventSetDescriptor for the given event set name, or null if not found.
	 */
	public EventSetDescriptor getEventSet(String pEventSetName, Logger pLogger) throws ELException {
		checkInitialized(pLogger);
		return (EventSetDescriptor) mEventSetByName.get(pEventSetName);
	}

	// -------------------------------------
	// Finding the public version of a method - if a PropertyDescriptor
	// is obtained for a non-public class that implements a public
	// interface, the read/write methods will be for the class, and
	// therefore inaccessible. To correct this, a version of the same
	// method must be found in a superclass or interface.
	// -------------------------------------

	/**
	 * Returns a publicly-accessible version of the given method, by searching for a public declaring class.
	 */
	static Method getPublicMethod(Method pMethod) {
		if (pMethod == null) {
			return null;
		}

		// See if the method is already available from a public class
		Class<?> cl = pMethod.getDeclaringClass();
		if (Modifier.isPublic(cl.getModifiers())) {
			return pMethod;
		}

		// Otherwise, try to find a public class that declares the method
		Method ret = getPublicMethod(cl, pMethod);
		if (ret != null) {
			return ret;
		} else {
			return pMethod;
		}
	}

	// -------------------------------------

	/**
	 * If the given class is public and has a Method that declares the same name and arguments as the given method, then that method is returned. Otherwise the
	 * superclass and interfaces are searched recursively.
	 */
	static Method getPublicMethod(Class<?> pClass, Method pMethod) {
		// See if this is a public class declaring the method
		if (Modifier.isPublic(pClass.getModifiers())) {
			try {
				Method m;
				try {
					m = pClass.getDeclaredMethod(pMethod.getName(), pMethod.getParameterTypes());
				} catch (java.security.AccessControlException ex) {
					// kludge to accommodate J2EE RI's default settings
					// TODO: see if we can simply replace
					// getDeclaredMethod() with getMethod() ...?
					m = pClass.getMethod(pMethod.getName(), pMethod.getParameterTypes());
				}
				if (Modifier.isPublic(m.getModifiers())) {
					return m;
				}
			} catch (NoSuchMethodException exc) {
			}
		}

		// Search the interfaces
		{
			Class<?>[] interfaces = pClass.getInterfaces();
			if (interfaces != null) {
				for (int i = 0; i < interfaces.length; i++) {
					Method m = getPublicMethod(interfaces[i], pMethod);
					if (m != null) {
						return m;
					}
				}
			}
		}

		// Search the superclass
		{
			Class<?> superclass = pClass.getSuperclass();
			if (superclass != null) {
				Method m = getPublicMethod(superclass, pMethod);
				if (m != null) {
					return m;
				}
			}
		}

		return null;
	}

	// -------------------------------------
}
